一、JNI 在 Android 中的角色
JNI(Java Native Interface)是 Java 虚拟机规范的一部分,定义了 Java 代码与 C/C++ 原生代码之间互操作的接口。在 Android 中,JNI 是连接 Java 世界(Android Framework、App 代码)和 Native 世界(C/C++ 库、硬件驱动、NDK 模块)的桥梁。
Android 系统本身大量使用 JNI:Framework 层通过 JNI 调用 HAL(硬件抽象层)实现对传感器、摄像头、音频等硬件的访问。Zygote 进程在 fork 每个应用进程时,会预先加载 JNI 库以减少应用启动时的开销。
JNI 在 Android 中的典型使用场景:
- 性能敏感:图像处理、音视频编解码、加密算法。
- 代码复用:移植已有的 C/C++ 库(如 ffmpeg、OpenSSL、SDL)。
- 安全需求:将加解密、反作弊等关键逻辑放在 native 层增加逆向难度。
- 系统调用:访问 Linux 底层 API(epoll、mmap、ptrace 等)。
二、JNI 数据类型映射
JNI 定义了一组与 Java 类型对应的 C/C++ 类型:
| Java 类型 |
JNI 类型 |
C/C++ 类型 |
描述 |
boolean |
jboolean |
unsigned char (8-bit) |
0 = false, nonzero = true |
byte |
jbyte |
signed char (8-bit) |
-128 to 127 |
char |
jchar |
unsigned short (16-bit) |
UTF-16 code unit |
short |
jshort |
signed short (16-bit) |
|
int |
jint |
signed int (32-bit) |
|
long |
jlong |
signed long long (64-bit) |
|
float |
jfloat |
float (32-bit IEEE 754) |
|
double |
jdouble |
double (64-bit IEEE 754) |
|
void |
void |
N/A |
|
Object |
jobject |
pointer |
所有 Java 对象的基类型 |
String |
jstring |
pointer |
Java String 对象 |
Class |
jclass |
pointer |
Java Class 对象 |
Throwable |
jthrowable |
pointer |
Java 异常对象 |
boolean[] |
jbooleanArray |
pointer |
|
int[] |
jintArray |
pointer |
|
引用类型是一个指针(在 ART 运行时中指向堆上的对象),不能直接当作 C 指针使用。访问 Java 数组中的数据需要通过 JNI 提供的 Get/Release 函数。
JNIEXPORT void JNICALL Java_com_example_NativeLib_processArray( JNIEnv *env, jobject thiz, jintArray array) {
jsize len = (*env)->GetArrayLength(env, array);
jint *elements = (*env)->GetIntArrayElements(env, array, NULL); if (elements == NULL) { return; }
for (int i = 0; i < len; i++) { elements[i] *= 2; }
(*env)->ReleaseIntArrayElements(env, array, elements, 0); }
|
GetPrimitiveArrayCritical 提供了更直接的访问方式(可能阻止 GC),配对使用的是 ReleasePrimitiveArrayCritical。Critical 版本与 Get/Release 版本的区别是:Critical 期间 GC 被暂停(对性能敏感的代码慎用),且不能在这两个调用之间调用其他 JNI 函数或阻塞。
三、局部引用与全局引用:引用表溢出
JNI 的引用管理是面试中的高频考点,也是 native 内存泄漏的常见来源。
3.1 三种引用类型
| 引用类型 |
创建方式 |
生命周期 |
使用场景 |
| 局部引用(Local Reference) |
大多数 JNI 函数自动创建 |
当前 native 方法返回时自动释放 |
临时使用的 Java 对象 |
| 全局引用(Global Reference) |
NewGlobalRef() |
显式调用 DeleteGlobalRef() 释放 |
跨多个 native 调用共享的对象 |
| 弱全局引用(Weak Global Reference) |
NewWeakGlobalRef() |
显式调用 DeleteWeakGlobalRef() 释放 |
可能被 GC 回收的对象引用 |
3.2 局部引用表溢出
每个 native 线程有一个局部引用表,默认容量通常是 512(ART 运行时)。如果在一个 native 方法中创建了大量局部引用而不释放,会抛出 JNI ERROR (app bug): local reference table overflow:
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList( JNIEnv *env, jobject thiz, jobjectArray strings) { jsize len = (*env)->GetArrayLength(env, strings); for (int i = 0; i < len; i++) { jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i); const char *utf = (*env)->GetStringUTFChars(env, str, NULL); (*env)->ReleaseStringUTFChars(env, str, utf); } }
|
正确的处理方式:
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList( JNIEnv *env, jobject thiz, jobjectArray strings) { jsize len = (*env)->GetArrayLength(env, strings); for (int i = 0; i < len; i++) { jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i); const char *utf = (*env)->GetStringUTFChars(env, str, NULL); process(utf); (*env)->ReleaseStringUTFChars(env, str, utf); (*env)->DeleteLocalRef(env, str); } }
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList2( JNIEnv *env, jobject thiz, jobjectArray strings) { jsize len = (*env)->GetArrayLength(env, strings); for (int i = 0; i < len; i++) { if ((*env)->PushLocalFrame(env, 16) < 0) { return; } jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i); const char *utf = (*env)->GetStringUTFChars(env, str, NULL); process(utf); (*env)->ReleaseStringUTFChars(env, str, utf); (*env)->PopLocalFrame(env, NULL); } }
|
3.3 全局引用
static jobject g_cached_object = NULL;
JNIEXPORT void JNICALL Java_com_example_NativeLib_init(JNIEnv *env, jobject thiz, jobject obj) { g_cached_object = (*env)->NewGlobalRef(env, obj); }
JNIEXPORT void JNICALL Java_com_example_NativeLib_cleanup(JNIEnv *env, jobject thiz) { if (g_cached_object != NULL) { (*env)->DeleteGlobalRef(env, g_cached_object); g_cached_object = NULL; } }
|
四、JNI_OnLoad 与动态注册
有两种注册 native 方法的方式:
4.1 静态注册(基于方法名约定)
JNIEXPORT jstring JNICALL Java_com_example_NativeLib_getMessage(JNIEnv *env, jobject thiz) { return (*env)->NewStringUTF(env, "Hello from JNI"); }
|
缺点:方法名太长,容易写错,且每次调用 native 方法时 VM 需要搜索符号表。
4.2 动态注册(通过 JNI_OnLoad)
static jstring native_getMessage(JNIEnv *env, jobject thiz) { return (*env)->NewStringUTF(env, "Hello from JNI"); }
static jint native_add(JNIEnv *env, jobject thiz, jint a, jint b) { return a + b; }
static const JNINativeMethod gMethods[] = { {"getMessage", "()Ljava/lang/String;", (void *)native_getMessage}, {"add", "(II)I", (void *)native_add}, };
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; }
jclass clazz = (*env)->FindClass(env, "com/example/NativeLib"); if (clazz == NULL) { return JNI_ERR; }
if ((*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) { return JNI_ERR; }
return JNI_VERSION_1_6; }
|
动态注册的优势:
- 方法名更简洁,不需要长命名。
- 首次加载时批量注册,后续调用查找快。
- 可以在
JNI_OnLoad 中做初始化工作(如缓存 MethodID)。
JNI 方法签名规则:
(参数类型签名)返回值类型签名
I = int, J = long, F = float, D = double, Z = boolean, V = void
Ljava/lang/String; = String 对象
[I = int[]
五、从 Native 调用 Java 方法
JNI 允许 native 代码调用 Java 方法。关键 API:GetMethodID/GetStaticMethodID + CallVoidMethod/CallStaticMethod 等。
static void callJavaCallback(JNIEnv *env, jobject callback_obj, const char *result) { jclass clazz = (*env)->GetObjectClass(env, callback_obj);
jmethodID methodId = (*env)->GetMethodID(env, clazz, "onResult", "(Ljava/lang/String;)V");
jstring jResult = (*env)->NewStringUTF(env, result);
(*env)->CallVoidMethod(env, callback_obj, methodId, jResult);
(*env)->DeleteLocalRef(env, jResult); (*env)->DeleteLocalRef(env, clazz); }
|
性能优化要点:FindClass、GetMethodID、GetFieldID 的调用开销较大(需要 JNI 内部字符串比较和查找),应该在 JNI_OnLoad 中一次性获取并缓存为全局引用:
static jclass g_callbackClass = NULL; static jmethodID g_onResultMethod = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6); jclass localClass = (*env)->FindClass(env, "com/example/Callback"); g_callbackClass = (*env)->NewGlobalRef(env, localClass); g_onResultMethod = (*env)->GetMethodID(env, g_callbackClass, "onResult", "(Ljava/lang/String;)V"); (*env)->DeleteLocalRef(env, localClass); return JNI_VERSION_1_6; }
|
六、Native 线程与 JavaVM
从 native 创建的线程(如通过 pthread 或 std::thread),JNIEnv 是线程局部存储的。新线程不能直接使用从其他线程传入的 JNIEnv 指针。必须调用 JavaVM::AttachCurrentThread() 获取当前线程的 JNIEnv:
static JavaVM *g_jvm = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) { g_jvm = vm; return JNI_VERSION_1_6; }
void *thread_func(void *arg) { JNIEnv *env; jint result = (*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL); if (result != JNI_OK) { return NULL; }
(*g_jvm)->DetachCurrentThread(g_jvm); return NULL; }
|
注意事项:
AttachCurrentThread 必须成对出现 DetachCurrentThread,否则线程退出后局部引用表无法被清理。
- 如果线程已经被 attached,再次调用
AttachCurrentThread 是安全的(no-op)。
七、Java 异常的 Native 处理
JNI 函数出错时,大多数不会通过返回值表示,而是在 Java 层设置异常。native 代码必须显式检查和清理这些异常:
JNIEXPORT void JNICALL Java_com_example_NativeLib_safeCall(JNIEnv *env, jobject thiz, jobject obj) { jclass clazz = (*env)->GetObjectClass(env, obj); jmethodID methodId = (*env)->GetMethodID(env, clazz, "nonExistentMethod", "()V");
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); jclass npeClass = (*env)->FindClass(env, "java/lang/NullPointerException"); (*env)->ThrowNew(env, npeClass, "Callback method not found"); return; }
(*env)->CallVoidMethod(env, obj, methodId); }
|
关键规则:大多数 JNI 函数在异常挂起时如果继续调用会失败(甚至崩溃)。通过 ExceptionCheck() 或 ExceptionOccurred() 检测异常,然后 ExceptionClear() 清除或 ThrowNew() 抛出新异常。
八、完整实践:Native 加密实现
#include <jni.h> #include <string.h> #include <openssl/aes.h>
static jbyteArray native_encrypt(JNIEnv *env, jobject thiz, jbyteArray data, jbyteArray key) { jsize dataLen = (*env)->GetArrayLength(env, data); jsize keyLen = (*env)->GetArrayLength(env, key);
if (keyLen != AES_BLOCK_SIZE) { jclass exClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); (*env)->ThrowNew(env, exClass, "Key must be 16 bytes (AES-128)"); return NULL; }
jbyte *dataBytes = (*env)->GetByteArrayElements(env, data, NULL); jbyte *keyBytes = (*env)->GetByteArrayElements(env, key, NULL);
jsize paddedLen = ((dataLen / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE; unsigned char *paddedData = malloc(paddedLen); memcpy(paddedData, (unsigned char *)dataBytes, dataLen); int padValue = paddedLen - dataLen; memset(paddedData + dataLen, padValue, padValue);
AES_KEY aesKey; AES_set_encrypt_key((const unsigned char *)keyBytes, 128, &aesKey);
unsigned char iv[AES_BLOCK_SIZE] = {0}; unsigned char *ciphertext = malloc(paddedLen); AES_cbc_encrypt(paddedData, ciphertext, paddedLen, &aesKey, iv, AES_ENCRYPT);
jbyteArray result = (*env)->NewByteArray(env, paddedLen); (*env)->SetByteArrayRegion(env, result, 0, paddedLen, (jbyte *)ciphertext);
free(ciphertext); free(paddedData); (*env)->ReleaseByteArrayElements(env, key, keyBytes, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, data, dataBytes, JNI_ABORT);
return result; }
static const JNINativeMethod gMethods[] = { {"encrypt", "([B[B)[B", (void *)native_encrypt}, };
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6); jclass clazz = (*env)->FindClass(env, "com/example/crypto/NativeCrypto"); (*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])); return JNI_VERSION_1_6; }
|
九、面试常问题目
Q1: 局部引用和全局引用的区别?什么情况下会导致 local reference table overflow?
局部引用在 native 方法返回时自动释放,全局引用需要显式 DeleteGlobalRef() 才能释放。在循环中创建大量局部引用(如遍历大数组时每次都 GetObjectArrayElement)而不手动 DeleteLocalRef,或在循环内不对引用进行释放,会导致局部引用表(默认 512 容量)溢出。解决方案是使用 PushLocalFrame/PopLocalFrame 或在循环内 DeleteLocalRef。
Q2: JNI_OnLoad 的作用是什么?什么时候需要实现它?
JNI_OnLoad 在 System.loadLibrary() 加载 .so 文件后由 VM 自动调用。主要用于:(1) 使用 RegisterNatives() 进行动态注册,替代冗长的 Java_xxx 命名;(2) 初始化全局引用(缓存 class、methodID、fieldID);(3) 缓存 JavaVM 指针供 native 线程使用;(4) 返回所需的 JNI 版本号。
Q3: 为什么 native 线程不能直接使用传入的 JNIEnv 指针?
JNIEnv 是线程局部存储的(thread-local),与创建它的线程绑定。如果在 Thread A 获取了一个 JNIEnv 指针,然后在 Thread B(通过 pthread_create 创建的 native 线程)中使用它,会出现两种情况:(1) 使用错误的线程局部数据导致崩溃或未定义行为;(2) 该 JNIEnv 对应的线程局部引用表是为 Thread A 管理的,Thread B 没有权限访问。正确方法是新线程调用 JavaVM::AttachCurrentThread() 获取自己的 JNIEnv。
Q4: JNI 调用 Java 方法时,methodID 可以跨线程使用吗?需要全局引用吗?
MethodID 和 FieldID 是进程范围内的,可以跨线程安全使用,不需要转换为全局引用。这是因为它们在 VM 内部是以指针或索引形式存在的,不受 GC 影响。但 class 引用(jclass)必须转换为全局引用才能跨线程使用或缓存——局部 jclass 在方法返回后可能失效。
参考源码路径:
- JNI 规范:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
- Android JNI Tips:
https://developer.android.com/training/articles/perf-jni
- AOSP 示例:
frameworks/base/core/jni/android_util_EventLog.cpp
- AOSP JNI 入口:
libnativehelper/include_jni/jni.h
- ART 运行时 JNI 实现:
art/runtime/jni/jni_internal.cc